iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
AI Engineering

從零打造情感感知 Agentic System:FSM 狀態機與 RAG 的整合實作系列 第 16 篇

Day 16:【狀態防禦篇】解決 API 失敗造成的好感度漏洞!實作「先 Preview、成功才落地」機制

  • 分享至 

  • xImage
  •  

前言

這個 Bug 前幾天測試時就露出了馬腳,但一直被我「假裝沒看到」拖延至今,今天決定一次把它解決!

串接 LLM API 時遇到 503 Service Unavailable 或網路突然「鏈結崩壞」完全是日常。以往系統是「算完好感度 ⇒ 直接更新 Firestore ⇒ 發送訊息」,只要 API 在半路拋 Exception,Firestore 裡的好感度就被偷加偷扣,對話卻沒發出去,導致嚴重的狀態錯亂。

這裡借用 Atomicity 的概念:把「產生回應」與「好感度落地」視為同一個應用層操作,只有 API 成功後才 Commit。確定拿到回應才寫入 Firestore,並用 preview_affection 的預覽值即時調製 Prompt,讓莉莉 Agent 被誇獎的當下就能傲嬌軟化,不再出現「下一輪才反應過來」的呆滯感!

今日目標

  1. 絕對防禦降臨:重構流程為「Preview 預覽 ⇒ 送 API ⇒ Success ⇒ Commit 落地」的兩階段機制。
  2. 修復修羅場 Bug:解決 API 撞到 503 時,Firestore 好感度偷跑、產生孤兒狀態的現象。
  3. 當輪零時差軟化:讓莉莉在同一輪對話中就能根據最新讚美/吐槽即時切換狀態。
  4. 領域展開(RAG 隔離):記憶檢索結果注入每輪獨立覆寫的 system_instruction,完全不污染對話庫。

第一關:app.py(交易防禦與 RAG 動態隔離)

核心改動程式碼:app.py

「Preview 預覽 ⇒ 送 API ⇒ Success ⇒ Commit 落地」

try:
    # 先發動意圖雷達判斷 (CHAT 或 TASK)
    intent = router.classify_intent(client, user_input)

    if intent == "CHAT":
        # 背景靜悄悄地感知情緒,換算成好感度增減
        delta = evaluator.evaluate_emotion(client, user_input)

        # 只在 CHAT 路線評分:TASK 是叫她算數學、寫程式,
        # 那種話沒有情緒溫度可言,拿去評分只是白花一次 API
        #
        # 這裡只 preview 不落地:好感度要等 send_message 成功之後才真的寫入。
        # 若先寫入而 API 掛掉(例如 503 過載),就會出現
        # 「角色沒有回應、好感度卻加了」的狀況,而且收不回來
        preview_affection = st.session_state.fsm.preview_affection(delta)

        # 用假設值取這一輪的 System Instruction,
        # 所以誇她的那句話當下就會讓她軟化,不會慢一拍
        base_instruction = st.session_state.fsm.get_system_instruction(preview_affection)

        # --- RAG 記憶檢索與 System Instruction 疊加 ---
        rag_context = rag_builder.build_rag_context(user_input)
        final_instruction = base_instruction + ("\n\n" + rag_context if rag_context else "")

        response = st.session_state.chat.send_message(
            user_input,
            config=types.GenerateContentConfig(system_instruction=final_instruction)
        )
        ai_reply = response.text

        # 確定拿到回應了,好感度才真的落地寫進 Firestore
        st.session_state.fsm.update_affection(delta)

第二關:fsm.py(純函數預覽與動態人設生成)

import db

class FSM:
    def __init__(self, character_id: str = "lily", default_affection: int = db.DEFAULT_AFFECTION):
        self.character_id = character_id
        # 自動向 Firebase 的 FSM 集合獲取記憶
        self.affection = db.get_fsm_affection(self.character_id, default_val=default_affection)

    def preview_affection(self, delta: int) -> int:
        """算出加上 delta 之後的好感度,但「不」改動自身、也不寫入資料庫"""
        return min(max(0, self.affection + delta), 100)

    def update_affection(self, delta: int) -> int:
        """確定 API 回覆成功後,才真正寫入記憶體與 Firestore 雲端"""
        self.affection = self.preview_affection(delta)
        db.save_fsm_affection(self.character_id, self.affection)
        return self.affection

    def get_state(self, affection: int | None = None) -> str:
        """根據好感度回傳當前狀態標籤(支援傳入 preview 預覽值)"""
        value = self.affection if affection is None else affection

        if value <= 15:
            return "HOSTILE"      # 防備警戒
        elif value <= 75:
            return "TSUNDERE"     # 傲嬌嘴硬
        else:
            return "IN_LOVE"      # 暴走列車

今日心得

前幾天一直在串基礎建設、搞 RAG 檢索,整體感真的乾到不行,完全沒啥打程式碼的激情。

直到今天把這套 FSM 狀態防禦給補起來。之前測試時最惱人的就是遇到 API 噴 503:好感度已經偷偷加進 Firestore 了,畫面卻因為 API 爛掉而沒印出訊息,搞得資料庫裡的她好像默默接受了我的讚美,但畫面上卻毫無反應,直接演變成幽靈狀態。

另一個很明顯的問題是反應遲鈍。我剛誇完她,舊邏輯讓她這一輪還在跟你在那邊「哼!笨蛋!」,要等到下一輪對話她才突然像神經傳導過來一樣變溫柔,超怪。

今天搞了這個 Preview + 兩階段 Commit 機制:

  • 503 拋錯? 直接攔截,好感度絕不偷跑。
  • 當輪破防? 直接拿預覽值去餵當輪 Prompt,誇獎丟過去的當下回應就直接切換成軟化狀態!

鐵人賽正式過半,雖然前幾天真的乾,但看著 Agent 漸漸成形,有爽喔!!


上一篇
# Day 15:【RAG 時間陷阱】Agent 怎麼知道那是多久以前的事?實作 Time-Aware 動態時間檢索 ## 前言
下一篇
Day 17:【Tool Use 觀念篇】慢活時間!搞懂 MCP 與 Function Calling 的差別
系列文
從零打造情感感知 Agentic System:FSM 狀態機與 RAG 的整合實作 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言